View Javadoc

1   /*
2    * Copyright  2000-2005 The Apache Software Foundation
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *
16   */
17  
18  package org.apache.tools.ant.taskdefs;
19  
20  import org.apache.tools.ant.BuildException;
21  import org.apache.tools.ant.Project;
22  import org.apache.tools.ant.Task;
23  import org.apache.tools.ant.util.FileUtils;
24  import org.apache.tools.ant.util.JavaEnvUtils;
25  
26  import java.io.File;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.PrintStream;
31  import java.net.HttpURLConnection;
32  import java.net.URL;
33  import java.net.URLConnection;
34  import java.util.Date;
35  
36  /***
37   * Gets a particular file from a URL source.
38   * Options include verbose reporting, timestamp based fetches and controlling
39   * actions on failures. NB: access through a firewall only works if the whole
40   * Java runtime is correctly configured.
41   *
42   * @since Ant 1.1
43   *
44   * @ant.task category="network"
45   */
46  public class Get extends Task {
47  
48      private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
49  
50      private URL source; // required
51      private File dest; // required
52      private boolean verbose = false;
53      private boolean useTimestamp = false; //off by default
54      private boolean ignoreErrors = false;
55      private String uname = null;
56      private String pword = null;
57  
58  
59  
60      /***
61       * Does the work.
62       *
63       * @exception BuildException Thrown in unrecoverable error.
64       */
65      public void execute() throws BuildException {
66  
67          //set up logging
68          int logLevel = Project.MSG_INFO;
69          DownloadProgress progress = null;
70          if (verbose) {
71              progress = new VerboseProgress(System.out);
72          }
73  
74          //execute the get
75          try {
76              doGet(logLevel, progress);
77          } catch (IOException ioe) {
78              log("Error getting " + source + " to " + dest);
79              if (!ignoreErrors) {
80                  throw new BuildException(ioe, getLocation());
81              }
82          }
83      }
84  
85      /***
86       * make a get request, with the supplied progress and logging info.
87       * All the other config parameters are set at the task level,
88       * source, dest, ignoreErrors, etc.
89       * @param logLevel level to log at, see {@link Project#log(String, int)}
90       * @param progress progress callback; null for no-callbacks
91       * @return true for a successful download, false otherwise.
92       * The return value is only relevant when {@link #ignoreErrors} is true, as
93       * when false all failures raise BuildExceptions.
94       * @throws IOException for network trouble
95       * @throws BuildException for argument errors, or other trouble when ignoreErrors
96       * is false.
97       */
98      public boolean doGet(int logLevel, DownloadProgress progress)
99              throws IOException {
100         if (source == null) {
101             throw new BuildException("src attribute is required", getLocation());
102         }
103 
104         if (dest == null) {
105             throw new BuildException("dest attribute is required", getLocation());
106         }
107 
108         if (dest.exists() && dest.isDirectory()) {
109             throw new BuildException("The specified destination is a directory",
110                     getLocation());
111         }
112 
113         if (dest.exists() && !dest.canWrite()) {
114             throw new BuildException("Can't write to " + dest.getAbsolutePath(),
115                     getLocation());
116         }
117         //dont do any progress, unless asked
118         if (progress == null) {
119             progress = new NullProgress();
120         }
121         log("Getting: " + source, logLevel);
122         log("To: " + dest.getAbsolutePath(), logLevel);
123 
124         //set the timestamp to the file date.
125         long timestamp = 0;
126 
127         boolean hasTimestamp = false;
128         if (useTimestamp && dest.exists()) {
129             timestamp = dest.lastModified();
130             if (verbose) {
131                 Date t = new Date(timestamp);
132                 log("local file date : " + t.toString(), logLevel);
133             }
134             hasTimestamp = true;
135         }
136 
137         //set up the URL connection
138         URLConnection connection = source.openConnection();
139         //modify the headers
140         //NB: things like user authentication could go in here too.
141         if (hasTimestamp) {
142             connection.setIfModifiedSince(timestamp);
143         }
144         // prepare Java 1.1 style credentials
145         if (uname != null || pword != null) {
146             String up = uname + ":" + pword;
147             String encoding;
148             //we do not use the sun impl for portability,
149             //and always use our own implementation for consistent
150             //testing
151             Base64Converter encoder = new Base64Converter();
152             encoding = encoder.encode(up.getBytes());
153             connection.setRequestProperty ("Authorization",
154                     "Basic " + encoding);
155         }
156 
157         //connect to the remote site (may take some time)
158         connection.connect();
159         //next test for a 304 result (HTTP only)
160         if (connection instanceof HttpURLConnection) {
161             HttpURLConnection httpConnection
162                     = (HttpURLConnection) connection;
163             long lastModified = httpConnection.getLastModified();
164             if (httpConnection.getResponseCode()
165                     == HttpURLConnection.HTTP_NOT_MODIFIED
166                 || (lastModified != 0 && hasTimestamp
167                 && timestamp >= lastModified)) {
168                 //not modified so no file download. just return
169                 //instead and trace out something so the user
170                 //doesn't think that the download happened when it
171                 //didn't
172                 log("Not modified - so not downloaded", logLevel);
173                 return false;
174             }
175             // test for 401 result (HTTP only)
176             if (httpConnection.getResponseCode()
177                     == HttpURLConnection.HTTP_UNAUTHORIZED)  {
178                 String message = "HTTP Authorization failure";
179                 if (ignoreErrors) {
180                     log(message, logLevel);
181                     return false;
182                 } else {
183                     throw new BuildException(message);
184                 }
185             }
186 
187         }
188 
189         //REVISIT: at this point even non HTTP connections may
190         //support the if-modified-since behaviour -we just check
191         //the date of the content and skip the write if it is not
192         //newer. Some protocols (FTP) don't include dates, of
193         //course.
194 
195         InputStream is = null;
196         for (int i = 0; i < 3; i++) {
197             //this three attempt trick is to get round quirks in different
198             //Java implementations. Some of them take a few goes to bind
199             //property; we ignore the first couple of such failures.
200             try {
201                 is = connection.getInputStream();
202                 break;
203             } catch (IOException ex) {
204                 log("Error opening connection " + ex, logLevel);
205             }
206         }
207         if (is == null) {
208             log("Can't get " + source + " to " + dest, logLevel);
209             if (ignoreErrors) {
210                 return false;
211             }
212             throw new BuildException("Can't get " + source + " to " + dest,
213                     getLocation());
214         }
215 
216         FileOutputStream fos = new FileOutputStream(dest);
217         progress.beginDownload();
218         boolean finished = false;
219         try {
220             byte[] buffer = new byte[100 * 1024];
221             int length;
222             while ((length = is.read(buffer)) >= 0) {
223                 fos.write(buffer, 0, length);
224                 progress.onTick();
225             }
226             finished = true;
227         } finally {
228             FileUtils.close(fos);
229             FileUtils.close(is);
230 
231             // we have started to (over)write dest, but failed.
232             // Try to delete the garbage we'd otherwise leave
233             // behind.
234             if (!finished) {
235                 dest.delete();
236             }
237         }
238         progress.endDownload();
239 
240         //if (and only if) the use file time option is set, then
241         //the saved file now has its timestamp set to that of the
242         //downloaded file
243         if (useTimestamp)  {
244             long remoteTimestamp = connection.getLastModified();
245             if (verbose)  {
246                 Date t = new Date(remoteTimestamp);
247                 log("last modified = " + t.toString()
248                         + ((remoteTimestamp == 0)
249                         ? " - using current time instead"
250                         : ""), logLevel);
251             }
252             if (remoteTimestamp != 0) {
253                 FILE_UTILS.setFileLastModified(dest, remoteTimestamp);
254             }
255         }
256 
257         //successful download
258         return true;
259     }
260 
261 
262     /***
263      * Set the URL to get.
264      *
265      * @param u URL for the file.
266      */
267     public void setSrc(URL u) {
268         this.source = u;
269     }
270 
271     /***
272      * Where to copy the source file.
273      *
274      * @param dest Path to file.
275      */
276     public void setDest(File dest) {
277         this.dest = dest;
278     }
279 
280     /***
281      * If true, show verbose progress information.
282      *
283      * @param v if "true" then be verbose
284      */
285     public void setVerbose(boolean v) {
286         verbose = v;
287     }
288 
289     /***
290      * If true, log errors but do not treat as fatal.
291      *
292      * @param v if "true" then don't report download errors up to ant
293      */
294     public void setIgnoreErrors(boolean v) {
295         ignoreErrors = v;
296     }
297 
298     /***
299      * If true, conditionally download a file based on the timestamp
300      * of the local copy.
301      *
302      * <p>In this situation, the if-modified-since header is set so
303      * that the file is only fetched if it is newer than the local
304      * file (or there is no local file) This flag is only valid on
305      * HTTP connections, it is ignored in other cases.  When the flag
306      * is set, the local copy of the downloaded file will also have
307      * its timestamp set to the remote file time.</p>
308      *
309      * <p>Note that remote files of date 1/1/1970 (GMT) are treated as
310      * 'no timestamp', and web servers often serve files with a
311      * timestamp in the future by replacing their timestamp with that
312      * of the current time. Also, inter-computer clock differences can
313      * cause no end of grief.</p>
314      * @param v "true" to enable file time fetching
315      */
316     public void setUseTimestamp(boolean v) {
317         useTimestamp = v;
318     }
319 
320 
321     /***
322      * Username for basic auth.
323      *
324      * @param u username for authentication
325      */
326     public void setUsername(String u) {
327         this.uname = u;
328     }
329 
330     /***
331      * password for the basic authentication.
332      *
333      * @param p password for authentication
334      */
335     public void setPassword(String p) {
336         this.pword = p;
337     }
338 
339     /**********************************************************************
340     * BASE 64 encoding of a String or an array of bytes.
341     *
342     * Based on RFC 1421.
343     *
344     *********************************************************************/
345 
346     protected static class Base64Converter {
347 
348         public final char[] alphabet = {
349             'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',  //  0 to  7
350             'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',  //  8 to 15
351             'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',  // 16 to 23
352             'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',  // 24 to 31
353             'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',  // 32 to 39
354             'o', 'p', 'q', 'r', 's', 't', 'u', 'v',  // 40 to 47
355             'w', 'x', 'y', 'z', '0', '1', '2', '3',  // 48 to 55
356             '4', '5', '6', '7', '8', '9', '+', '/'}; // 56 to 63
357 
358         public String encode(String s) {
359             return encode(s.getBytes());
360         }
361 
362         public String encode(byte[] octetString) {
363             int bits24;
364             int bits6;
365 
366             char[] out = new char[((octetString.length - 1) / 3 + 1) * 4];
367             int outIndex = 0;
368             int i = 0;
369 
370             while ((i + 3) <= octetString.length) {
371                 // store the octets
372                 bits24 = (octetString[i++] & 0xFF) << 16;
373                 bits24 |= (octetString[i++] & 0xFF) << 8;
374                 bits24 |= octetString[i++];
375 
376                 bits6 = (bits24 & 0x00FC0000) >> 18;
377                 out[outIndex++] = alphabet[bits6];
378                 bits6 = (bits24 & 0x0003F000) >> 12;
379                 out[outIndex++] = alphabet[bits6];
380                 bits6  = (bits24 & 0x00000FC0) >> 6;
381                 out[outIndex++] = alphabet[bits6];
382                 bits6 = (bits24 & 0x0000003F);
383                 out[outIndex++] = alphabet[bits6];
384             }
385             if (octetString.length - i == 2) {
386                 // store the octets
387                 bits24 = (octetString[i] & 0xFF) << 16;
388                 bits24 |= (octetString[i + 1] & 0xFF) << 8;
389                 bits6 = (bits24 & 0x00FC0000) >> 18;
390                 out[outIndex++] = alphabet[bits6];
391                 bits6 = (bits24 & 0x0003F000) >> 12;
392                 out[outIndex++] = alphabet[bits6];
393                 bits6 = (bits24 & 0x00000FC0) >> 6;
394                 out[outIndex++] = alphabet[bits6];
395 
396                 // padding
397                 out[outIndex++] = '=';
398             } else if (octetString.length - i == 1) {
399                 // store the octets
400                 bits24 = (octetString[i] & 0xFF) << 16;
401                 bits6 = (bits24 & 0x00FC0000) >> 18;
402                 out[outIndex++] = alphabet[bits6];
403                 bits6 = (bits24 & 0x0003F000) >> 12;
404                 out[outIndex++] = alphabet[bits6];
405 
406                 // padding
407                 out[outIndex++] = '=';
408                 out[outIndex++] = '=';
409             }
410             return new String(out);
411         }
412     }
413 
414     public interface DownloadProgress {
415         /***
416          * begin a download
417          */
418         public void beginDownload();
419 
420         /***
421          * tick handler
422          *
423          */
424         public void onTick();
425 
426         /***
427          * end a download
428          */
429         public void endDownload();
430     }
431 
432     /***
433      * do nothing with progress info
434      */
435     public static class NullProgress implements DownloadProgress {
436 
437         /***
438          * begin a download
439          */
440         public void beginDownload() {
441 
442         }
443 
444         /***
445          * tick handler
446          *
447          */
448         public void onTick() {
449         }
450 
451         /***
452          * end a download
453          */
454         public void endDownload() {
455 
456         }
457     }
458 
459     /***
460      * verbose progress system prints to some output stream
461      */
462     public static class VerboseProgress implements DownloadProgress  {
463         private int dots = 0;
464         PrintStream out;
465 
466         public VerboseProgress(PrintStream out) {
467             this.out = out;
468         }
469 
470         /***
471          * begin a download
472          */
473         public void beginDownload() {
474             dots = 0;
475         }
476 
477         /***
478          * tick handler
479          *
480          */
481         public void onTick() {
482             out.print(".");
483             if (dots++ > 50) {
484                 out.flush();
485                 dots = 0;
486             }
487         }
488 
489         /***
490          * end a download
491          */
492         public void endDownload() {
493             out.println();
494             out.flush();
495         }
496     }
497 
498 }